using System;
using System.IO;
using System.Xml;
using System.Collections.Generic;
using System.Drawing;
using Clandestine.Base;
using Clandestine.Graphics;

namespace Clandestine.Engines.Field
{
	// IWatchable: Doesn't fire the event at the moment; implements for future compatibility and to work with TreeView nicely.
	public class Object : AbstractObject, IWatchable
	{
		public StaticLayerGroup FloorLayers;
		public StaticLayerGroup PerpendicularLayers; // Other two can use megaquads if you want, but this MUST NOT. :) plan on making custom RenderList for perps to do the ordering
		public StaticLayerGroup SkyLayers;
		public Map Map { get; private set; }
		public Level Level { get; private set; }
		public string Name = string.Empty;
		public event Changed Changed;
		private Sprite selectionSprite;
		
		private bool visible = true;
		public bool Visible
		{
			get { return visible; } 
			set
			{
				visible = value;
				
				FloorLayers.Visible = value;
				PerpendicularLayers.Visible = value;
				SkyLayers.Visible = value;
				
				if (Changed != null)
					Changed(this);
			}
		}
		
		public bool Dirty
		{
			get { return (this.FloorLayers.Dirty || this.PerpendicularLayers.Dirty || this.SkyLayers.Dirty); } 
			set
			{
				this.FloorLayers.Dirty = true;
				this.SkyLayers.Dirty = true;
				this.PerpendicularLayers.Dirty = true;
				
				if (Changed != null)
					Changed(this);
			}
		}
		
		// The position of the object's top-left tile.
		public Point Position
		{
			get { return new Point(InternalPosition.X + this.MinX, InternalPosition.Y + this.MinY); } 
			set { InternalPosition = new Point(value.X - this.MinX, value.Y - this.MinY); }
		}
		
		// The position of the object's (0,0) tile.
		private Point internalPosition;
		private Point InternalPosition 
		{
			 get { return internalPosition; }
			 set 
			 {
			 	FloorLayers.Position = value;
			 	PerpendicularLayers.Position = value;
			 	SkyLayers.Position = value;
			 	
			 	if (selectionSprite != null) // looks ugly here, but gotta do what you gotta do! D:
			 	{
			 		selectionSprite.Transform.X = value.X * this.Map.GridSize;
			 		selectionSprite.Transform.Y = value.Y * this.Map.GridSize;
			 	}
			 	
			 	this.internalPosition = value;
				
				if (Changed != null)
					Changed(this);
			 }
		}
		
		public Size Size { get { return new Size(MaxX - MinX + 1, MaxY - MinY + 1); } } // +1 cos tile counting starts at 0
		
		// Possible todo: cache these
		// The LayerGroup caches the values (or it should...) so tbf the occasional two calls to .Min/.Max are not too bad.
		public int MinX { get { return Math.Min(FloorLayers.MinX, Math.Min(SkyLayers.MinX, PerpendicularLayers.MinX)); } }
		public int MinY { get { return Math.Min(FloorLayers.MinY, Math.Min(SkyLayers.MinY, PerpendicularLayers.MinY)); } }
		public int MaxX { get { return Math.Max(FloorLayers.MinX + FloorLayers.Size.Width, Math.Max(SkyLayers.MinX + SkyLayers.Size.Width, PerpendicularLayers.MinX + PerpendicularLayers.Size.Width)); } }
		public int MaxY { get { return Math.Max(FloorLayers.MinY + FloorLayers.Size.Height, Math.Max(SkyLayers.MinY + SkyLayers.Size.Height, PerpendicularLayers.MinY + PerpendicularLayers.Size.Height)); } }
		
		public Object(Level level, Map map) : base(level)
		{
			this.Map = map;
			this.FloorLayers = new StaticLayerGroup(this.FloorRenderList, Map, this);
			this.PerpendicularLayers = new StaticLayerGroup(this.PerpendicularRenderList, Map, this);
			this.SkyLayers = new StaticLayerGroup(this.SkyRenderList, Map, this);
			this.Level = level;
			
			// Do depth munge!
			// (look at position of pixel in perp layers that is lowest down the screen -- where this is above
			//  a lowest pixel of the perp layers of another object, this object should have a higher depth.)
			this.Changed += delegate(IWatchable obj) { 
				this.FloorLayers.Depth = this.PerpendicularLayers.Depth = this.SkyLayers.Depth
					= (InternalPosition.Y + MaxY + 1) * this.Map.GridSize; };
			
			this.FloorLayers.Changed += delegate(IWatchable obj) { this.Changed(this.FloorLayers); };
			this.SkyLayers.Changed += delegate(IWatchable obj) { this.Changed(this.SkyLayers); };
			this.PerpendicularLayers.Changed += delegate(IWatchable obj) { this.Changed(this.PerpendicularLayers); };
		}
		
		public void SaveToFile(string filename)
		{
			//
			// -- Object Image --
			//
		
			int minX = this.MinX;
			int minY = this.MinY; // cache these
		
			// Reposition the layer groups (save their positions...)
			Point posSky, posPerp, posFloor;
			posSky = SkyLayers.Position;
			posPerp = PerpendicularLayers.Position;
			posFloor = FloorLayers.Position;
			SkyLayers.PositionTopLeft(new Point(0, 0), minX, minY);
			PerpendicularLayers.PositionTopLeft(new Point(0, 0), minX, minY);
			FloorLayers.PositionTopLeft(new Point(0, 0), minX, minY);
			
			// Construct a temporary render list and add the layergroup's sprites to it
			// Gotta munge depths >.>'
			RenderList list = new RenderList(this.Map.Graphics);
			List<Renderable> renSky = SkyLayers.GetRenderables();
			List<Renderable> renPerp = PerpendicularLayers.GetRenderables();
			List<Renderable> renFloor = FloorLayers.GetRenderables();
			int count = renSky.Count + renPerp.Count + renFloor.Count;
			foreach (Renderable r in renSky)
				list.Add(count--, r);
			foreach (Renderable r in renPerp)
				list.Add(count--, r);
			foreach (Renderable r in renFloor)
				list.Add(count--, r);	
				
			// Render to file
			this.Map.Graphics.RenderToFile(list, new Size(this.Size.Width * this.Map.GridSize, this.Size.Height * this.Map.GridSize), filename + ".png");
		
			//
			// -- XML --
			//
			
			// Setup XMLs!
			XmlWriter xml = XmlWriter.Create(filename);
			xml.WriteStartElement("external-object");
			
			// Write <spritesheets>
			this.Map.Spritesheets.Save(xml, this.GetSpritesheetsUsed());
			
			// Write <object>
			this.Save(xml);
			
			// Cleanup XMLs!
			xml.WriteEndElement();
			xml.Close();
			
			// Just reset the layergroups
			SkyLayers.Position = posSky;
			PerpendicularLayers.Position = posPerp;
			FloorLayers.Position = posFloor;
		}
		
		public IEnumerable<Spritesheet> GetSpritesheetsUsed()
		{
			foreach (Spritesheet s in this.SkyLayers.GetSpritesheetsUsed())
				yield return s;
			foreach (Spritesheet s in this.PerpendicularLayers.GetSpritesheetsUsed())
				yield return s;
			foreach (Spritesheet s in this.FloorLayers.GetSpritesheetsUsed())
				yield return s;
		}
		
		// returns a list of the spritesheets used
		public void Save(XmlWriter xml)
		{
			xml.WriteStartElement("object");
			xml.WriteAttributeString("name", this.Name);
			xml.WriteAttributeString("position-x", this.InternalPosition.X.ToString());
			xml.WriteAttributeString("position-y", this.InternalPosition.Y.ToString());
			
			// LayerGroups
			SkyLayers.Save(xml, "sky");
			PerpendicularLayers.Save(xml, "perpendicular");
			FloorLayers.Save(xml, "floor");
			
			xml.WriteEndElement();
		}
		
		public void Load(string filename)
		{
			XmlReader xml = XmlReader.Create(filename);
			
			// Remapping
			// To account for differences between spritesheet numbers used in file and what they're loaded as.
			Dictionary<int, int> remapping = new Dictionary<int, int>();
			
			while (xml.Read())
			{
				if (xml.NodeType == XmlNodeType.Element)
                {
                    switch (xml.Name)
                    {
                    	case "spritesheets":
                    		remapping = this.Map.Spritesheets.Load(xml);
                    		break;
                    	case "object":
                    		if (remapping.Count == 0)
                    			throw new Exception("<object> before <spritesheets> in an external object file! (maybe empty <spritesheets>?)");
                    			
                    		this.Load(xml, remapping);
                    		break;
                    }
               	} else if ((xml.NodeType == XmlNodeType.EndElement) && (xml.Name == "external-object"))
                	return;
			}
		}
		
		public void Load(XmlReader xml, Dictionary<int, int> remapping)
		{
			while (((xml.NodeType != XmlNodeType.Element) || (xml.Name != "object")) && xml.Read());
			if (xml.EOF)
				throw new Exception("No <object> in given file.");
				
			// Get Attributes
			InternalPosition = new Point(int.Parse(xml.GetAttribute("position-x")), int.Parse(xml.GetAttribute("position-y")));
			Name = xml.GetAttribute("name");
			
			// And everything else
			while (xml.Read())
			{
				if (xml.NodeType == XmlNodeType.Element)
                {
                    switch (xml.Name)
                    {
                    	case "floor":
							this.FloorLayers.Load(xml, "floor", InternalPosition, remapping);
                    		break;
                    	case "perpendicular":
                    		this.PerpendicularLayers.Load(xml, "perpendicular", InternalPosition, remapping);
                    		break;
                    	case "sky":
                    		this.SkyLayers.Load(xml, "sky", InternalPosition, remapping);
                    		break;
                    }
               	} else if ((xml.NodeType == XmlNodeType.EndElement) && (xml.Name == "object"))
                	return;
			}
			
			if (this.Changed != null)
				this.Changed(this);
		}	
		
		public void Select()
		{	
			Deselect();
			int width = this.Size.Width * this.Map.GridSize;
			int height = this.Size.Height * this.Map.GridSize;
			
			// Make a box the same size as us.
			Bitmap bmpBox = new Bitmap(width, height);
			System.Drawing.Graphics g = System.Drawing.Graphics.FromImage((Image)bmpBox);
			g.DrawRectangle(new Pen(Color.Black, 1f), new Rectangle(0, 0, width-1, height-1));
			g.Flush();
			g.Dispose();
			
			// Make into a sprite & show.
			// Todo: for now this is in the Default render list, and it's working just fine. Move to this.SkyRenderList or w/e?
			this.selectionSprite = new Sprite(this.Map.Graphics, Guid.NewGuid().ToString(), bmpBox);
			this.selectionSprite.Transform.X = this.Position.X * this.Map.GridSize;
			this.selectionSprite.Transform.Y = this.Position.Y * this.Map.GridSize;
		}
		
		public void Deselect()
		{
			if (this.selectionSprite != null)
				this.selectionSprite.Dispose();
		}
		
		public void Dispose()
		{
			SkyLayers.Dispose();
			PerpendicularLayers.Dispose();
			FloorLayers.Dispose();
			if (selectionSprite != null)
				selectionSprite.Dispose();
		}
		
		public void RedrawDirty()
		{
			FloorLayers.RedrawDirty();
			PerpendicularLayers.RedrawDirty();
			SkyLayers.RedrawDirty();
		}
	}
}

